package org.bouncycastle.jce.provider;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CRLException;
import java.security.cert.Certificate;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.x509.CertificateList;
import org.bouncycastle.asn1.x509.TBSCertList;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.asn1.x509.X509Extensions;
import org.bouncycastle.jce.X509Principal;

/**
 * The following extensions are listed in RFC 2459 as relevant to CRLs
 * 
 * Authority Key Identifier Issuer Alternative Name CRL Number Delta CRL Indicator (critical) Issuing Distribution Point (critical)
 */
public class X509CRLObject extends X509CRL {
    private CertificateList c;

    public X509CRLObject(CertificateList c) {
        this.c = c;
    }

    /**
     * Will return true if any extensions are present and marked as critical as we currently dont handle any extensions!
     */
    public boolean hasUnsupportedCriticalExtension() {
        Set extns = getCriticalExtensionOIDs();
        if (extns != null && !extns.isEmpty()) {
            return true;
        }

        return false;
    }

    private Set getExtensionOIDs(boolean critical) {
        if (this.getVersion() == 2) {
            HashSet set = new HashSet();
            X509Extensions extensions = c.getTBSCertList().getExtensions();
            Enumeration e = extensions.oids();

            while (e.hasMoreElements()) {
                DERObjectIdentifier oid = (DERObjectIdentifier) e.nextElement();
                X509Extension ext = extensions.getExtension(oid);

                if (critical == ext.isCritical()) {
                    set.add(oid.getId());
                }
            }

            return set;
        }

        return null;
    }

    public Set getCriticalExtensionOIDs() {
        return getExtensionOIDs(true);
    }

    public Set getNonCriticalExtensionOIDs() {
        return getExtensionOIDs(false);
    }

    public byte[] getExtensionValue(String oid) {
        X509Extensions exts = c.getTBSCertList().getExtensions();

        if (exts != null) {
            X509Extension ext = exts.getExtension(new DERObjectIdentifier(oid));

            if (ext != null) {
                ByteArrayOutputStream bOut = new ByteArrayOutputStream();
                DEROutputStream dOut = new DEROutputStream(bOut);

                try {
                    dOut.writeObject(ext.getValue());

                    return bOut.toByteArray();
                } catch (Exception e) {
                    throw new RuntimeException("error encoding " + e.toString());
                }
            }
        }

        return null;
    }

    public byte[] getEncoded() throws CRLException {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        DEROutputStream dOut = new DEROutputStream(bOut);

        try {
            dOut.writeObject(c);

            return bOut.toByteArray();
        } catch (IOException e) {
            throw new CRLException(e.toString());
        }
    }

    public void verify(PublicKey key) throws CRLException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException,
            SignatureException {
        verify(key, BouncyCastleProvider.PROVIDER_NAME);
    }

    public void verify(PublicKey key, String sigProvider) throws CRLException, NoSuchAlgorithmException, InvalidKeyException,
            NoSuchProviderException, SignatureException {
        if (!c.getSignatureAlgorithm().equals(c.getTBSCertList().getSignature())) {
            throw new CRLException("Signature algorithm on CertifcateList does not match TBSCertList.");
        }

        Signature sig = Signature.getInstance(getSigAlgName(), sigProvider);

        sig.initVerify(key);
        sig.update(this.getTBSCertList());
        if (!sig.verify(this.getSignature())) {
            throw new SignatureException("CRL does not verify with supplied public key.");
        }
    }

    public int getVersion() {
        return c.getVersion();
    }

    public Principal getIssuerDN() {
        return new X509Principal(c.getIssuer());
    }

    public X500Principal getIssuerX500Principal() {
        try {
            ByteArrayOutputStream bOut = new ByteArrayOutputStream();
            ASN1OutputStream aOut = new ASN1OutputStream(bOut);

            aOut.writeObject(c.getIssuer());

            return new X500Principal(bOut.toByteArray());
        } catch (IOException e) {
            throw new IllegalStateException("can't encode issuer DN");
        }
    }

    public Date getThisUpdate() {
        return c.getThisUpdate().getDate();
    }

    public Date getNextUpdate() {
        if (c.getNextUpdate() != null) {
            return c.getNextUpdate().getDate();
        }

        return null;
    }

    public X509CRLEntry getRevokedCertificate(BigInteger serialNumber) {
        TBSCertList.CRLEntry[] certs = c.getRevokedCertificates();

        if (certs != null) {
            for (int i = 0; i < certs.length; i++) {
                if (certs[i].getUserCertificate().getValue().equals(serialNumber)) {
                    return new X509CRLEntryObject(certs[i]);
                }
            }
        }

        return null;
    }

    public Set getRevokedCertificates() {
        TBSCertList.CRLEntry[] certs = c.getRevokedCertificates();

        if (certs != null) {
            HashSet set = new HashSet();
            for (int i = 0; i < certs.length; i++) {
                set.add(new X509CRLEntryObject(certs[i]));

            }

            return set;
        }

        return null;
    }

    public byte[] getTBSCertList() throws CRLException {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        DEROutputStream dOut = new DEROutputStream(bOut);

        try {
            dOut.writeObject(c.getTBSCertList());

            return bOut.toByteArray();
        } catch (IOException e) {
            throw new CRLException(e.toString());
        }
    }

    public byte[] getSignature() {
        return c.getSignature().getBytes();
    }

    public String getSigAlgName() {
        Provider prov = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
        String algName = prov.getProperty("Alg.Alias.Signature." + this.getSigAlgOID());

        if (algName != null) {
            return algName;
        }

        Provider[] provs = Security.getProviders();

        //
        // search every provider looking for a real algorithm
        //
        for (int i = 0; i != provs.length; i++) {
            algName = provs[i].getProperty("Alg.Alias.Signature." + this.getSigAlgOID());
            if (algName != null) {
                return algName;
            }
        }

        return this.getSigAlgOID();
    }

    public String getSigAlgOID() {
        return c.getSignatureAlgorithm().getObjectId().getId();
    }

    public byte[] getSigAlgParams() {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();

        if (c.getSignatureAlgorithm().getParameters() != null) {
            try {
                DEROutputStream dOut = new DEROutputStream(bOut);

                dOut.writeObject(c.getSignatureAlgorithm().getParameters());
            } catch (Exception e) {
                throw new RuntimeException("exception getting sig parameters " + e);
            }

            return bOut.toByteArray();
        }

        return null;
    }

    /**
     * Returns a string representation of this CRL.
     * 
     * @return a string representation of this CRL.
     */
    public String toString() {
        return "X.509 CRL";
    }

    /**
     * Checks whether the given certificate is on this CRL.
     * 
     * @param cert
     *            the certificate to check for.
     * @return true if the given certificate is on this CRL, false otherwise.
     */
    public boolean isRevoked(Certificate cert) {
        if (!cert.getType().equals("X.509")) {
            throw new RuntimeException("X.509 CRL used with non X.509 Cert");
        }

        TBSCertList.CRLEntry[] certs = c.getRevokedCertificates();

        if (certs != null) {
            BigInteger serial = ((X509Certificate) cert).getSerialNumber();

            for (int i = 0; i < certs.length; i++) {
                if (certs[i].getUserCertificate().getValue().equals(serial)) {
                    return true;
                }
            }
        }

        return false;
    }
}
